Android源码 —— NestedScrolling

NestedScrolling常用在嵌套滚动的场景,比较常见的是使用CoordinateLayout实现比较炫酷的联合滚动效果,其内部也是借助了NestedScrollingChild和NestedScrollingParent这套机制。

NestedScrollingChild和NestedScrollingParent

NestedScrollingChild NestedScrollingParent
startNestedScroll onStartNestedScroll
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
dispatchNestedPreFling onNestedPreFling
dispatchNestedFling onNestedFling
stopNestedScroll onStopNestedScroll

NestedScrollingChild接口中的方法均为主动方法,需要我们在实现类中主动调用,而NestedScrollingParent的方法基本都是回调方法。这也是NestedScrolling机制的一个体现,子View作为NestedScrolling事件传递的主动方,父View作为接收方。父View决定是移动子View控件,还是把移动偏移量交给子View,让其滚动内容。

NestedScrolling事件传递过程:

子View的事件处理过程都在onTouchEvent()。

  1. 子View在action down中执行startNestedScroll()启动联动流程,并设置滚动的方向;
  2. 父View在onStartNestedScroll()中根据传来的方向,决定是否联动,返回结果;
  3. 子View在action move中计算移动偏移量,执行dispatchNestedPreScroll(),将偏移情况告诉父View;
  4. 父View在onNestedPreScroll()中接收子View传来的偏移量,计算需要消耗的偏移量,即移动子View的距离;
  5. 子View计算父View消费后剩下的偏移量,在这个余量基础上计算子View还能消费多少,并把消费情况通过dispatchNestedScroll()告诉父View;
  6. 父View在onNestedScroll()中根据偏移量进行相应处理;
  7. 事件结束,子View在action up中执行stopNestedScroll()结束联动流程,父View的onStopNestedScroll()得到响应,事件传递完成。

上述联动过程的传递,通过NestedScrollingChildHelper和NestedScrollingParentHelper这爷俩就可简单实现,里面封装了很多实现细节,让我们开发过程更高效。

fling过程和上述相同,可以通过示例代码了解。

下面通过代码讲一讲上述流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class ChildView implements NestedScrollingChild { 

@Override
public boolean onTouchEvent(MotionEvent event) {
    boolean result = false;

    MotionEvent trackedEvent = MotionEvent.obtain(event);

    final int action = MotionEventCompat.getActionMasked(event);

    if (action == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }

    int y = (int) event.getY();
    event.offsetLocation(0, mNestedYOffset);

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mLastMotionY = y;
            // 1.开始嵌套滚动,并确定滚动方向
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
            result = super.onTouchEvent(event);
            break;
       
        case MotionEvent.ACTION_MOVE:
            int deltaY = mLastMotionY - y;
           
            // 3.子View计算偏移量传给父View
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                // mScrollConsumed 保存被parent消费的尺寸
                deltaY -= mScrollConsumed[1];
                trackedEvent.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }

            mLastMotionY = y - mScrollOffset[1];

// 5.根据父View消费后的偏移余量,计算自己还能用多少,还能再剩下多少给父View继续使用
            int oldY = getScrollY();
            int newScrollY = Math.max(0, oldY + deltaY);
            int dyConsumed = newScrollY - oldY;
            int dyUnconsumed = deltaY - dyConsumed;

            if (dispatchNestedScroll(0, dyConsumed, 0, dyUnconsumed, mScrollOffset)) {
                mLastMotionY -= mScrollOffset[1];
                trackedEvent.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }

            result = super.onTouchEvent(trackedEvent);
            trackedEvent.recycle();
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
        // 7.结束流程
            stopNestedScroll();
            result = super.onTouchEvent(event);
            break;
    }
    return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ParentView implements NestedScrollParent { 

// 2.父View根据传来的方向,决定是否联动
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

// 4.计算需要消耗的偏移量,移动子View
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    if (dy > 0) {
int dyCanConsumed = mTargetCurOffset - mTargetEndOffset;
if (dy >= dyCanConsumed) {
consumed[1] = dyCanConsumed;
moveTo(mTargetEndOffset);
} else {
consumed[1] = dy;
moveBy(-dy);
}
}
}

// 6.父View根据子View的计算结果,做相关处理
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) {
moveBy(-dyUnconsumed);
}
}
}

相关问题:

1.子View主动触发一个事件,父View的对应方法就能响应,那父子View的联动关系是如何确定的?
看下NestScrollingChildHelper#startNestedScroll()的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean startNestedScroll(int axes) { 
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;

        // 向上遍历找parent,直到找到实现NestScrollParentHelper.onStartNestScroll()返回true的parent
        // 注意,ViewGroup也实现了NestScrollParentHelper,但是onStartNestScroll()返回false
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

相关知识点:

1.View#canScrollVertically()

判断View在垂直方向是否可以上下滚动。
其中:
View#canScrollVertically(1),return true ——可以向上滚动,反之,不可以
View#canScrollVertically(-1),return true ——可以向下滚动,反之,不可以

2.View#computeVerticalScrollOffset()

判断View的内容在垂直方向上滚动的距离。返回值:0~***,均为正值。
比如WebView,起始状态为0,内容向上滚动后,为某个正值。

3.View#computeVerticalScrollRange()

View内容的总高度。

4.View#computeVerticalScrollExtent()
View在屏幕区域内显示的高度。